﻿using MongoDB.Bson;
using MongoDB.Bson.Serialization;
using MongoDB.Driver;
using MongoDB.Driver.GridFS;
using Sustainalytics.Entities;
using Sustainalytics.Utils;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;

namespace Sustainalytics.DataLayer.Shared
{
    public class RepositoryCollectionWithFile<T> : IRepositoryCollection<T> where T : IEntity, IFileContent
    {
        private readonly IMongoCollection<T> _collection;
        private readonly IMongoDatabase _database;
        private readonly IRepositoryCollection<T> _repository;
        private readonly MongoGridFSHelper mongoGridFSHelper;
        private readonly string _collectionName;
        private readonly string _databaseName;

        static RepositoryCollectionWithFile()
        {
            if (!BsonClassMap<T>.IsClassMapRegistered(typeof(T)))
            {
                BsonClassMap.RegisterClassMap<T>(cm =>
                {
                    cm.AutoMap();
                    cm.UnmapProperty(c => c.FileContent);
                    cm.UnmapProperty(c => c.MimeType);
                });
            }
        }

        public RepositoryCollectionWithFile(IRepositoryCollection<T> repository)
        {
            _repository = repository;
            _collection = _repository.Collection();
            _database = _collection.Database;
        }

        public RepositoryCollectionWithFile(string mongoDbUrl, string collectionName = null)
            : this(new RepositoryCollection<T>(mongoDbUrl, collectionName))
        {
            _collectionName = collectionName;

            if (string.IsNullOrEmpty(mongoDbUrl))
                throw new ArgumentException("mongoDbUrl");

            var url = new MongoUrl(mongoDbUrl);
            if (string.IsNullOrEmpty(url.DatabaseName))
                throw new ArgumentException("databaseName");

            var mongoClient = new MongoClient(mongoDbUrl);
            var database = mongoClient.GetDatabase(url.DatabaseName);
            _databaseName = url.DatabaseName;
            mongoGridFSHelper = new MongoGridFSHelper(database);
        }

      
        public virtual bool Drop()
        {

            _collection.Database.DropCollection(_collectionName);
            return true;
        }

        public bool DropAndCreate()
        {
            throw new NotImplementedException();
        }

        public long UpdateBatch(IEnumerable<Tuple<string, object, string, object, bool>> updates)
        {
            throw new NotImplementedException();
        }

        public bool Create(T entity)
        {
            var ok = false;
            try
            {
                if (entity.Id == Guid.Empty)
                {
                    throw new ArgumentOutOfRangeException("entity");
                }

                var gridFSResult = mongoGridFSHelper.OpenUploadStream(entity.FileName, entity.Id, entity.MimeType);
                if (gridFSResult != null && gridFSResult.Item1 != null && gridFSResult.Item2 != Guid.Empty)
                {
                    using (gridFSResult.Item1)
                        gridFSResult.Item1.Write(entity.FileContent, 0, entity.FileContent.Count());

                    ok = _repository.Create(entity);
                }
            }
            catch (Exception)
            {
                ok = false;
                throw;
            }
            finally // clean up if failures
            {
                if (!ok)
                {
                    _repository.Delete(entity.Id);
                    mongoGridFSHelper.DeleteFile(entity.Id);
                }
            }
            return ok;
        }

        public bool CreateOrUpdate(T entity)
        {
            var ok = false;
            try
            {
                if (entity.Id == Guid.Empty)
                {
                    throw new ArgumentOutOfRangeException("entity");
                }

                var gridFSResult = mongoGridFSHelper.UpdateFile(entity.FileName, entity.Id, entity.Id,
                    entity.MimeType);
                if (gridFSResult != null && gridFSResult.Item1 != null && gridFSResult.Item2 != Guid.Empty)
                {
                    using (gridFSResult.Item1)
                        gridFSResult.Item1.Write(entity.FileContent, 0, entity.FileContent.Count());

                    ok = _repository.CreateOrUpdate(entity);
                }
            }
            catch (Exception)
            {
                ok = false;
                throw;
            }
            finally // clean up if failures
            {
                if (!ok)
                {
                    _repository.Delete(entity.Id);
                    mongoGridFSHelper.DeleteFile(entity.Id);
                }
            }

            return ok;
        }

        public T Read(Guid id)
        {
            try
            {
                var result = _repository.Read(id);
                if (result != null)
                {
                    var fi = mongoGridFSHelper.FindOneById(id);
                    if (fi != null)
                    {
                        result.FileContent = mongoGridFSHelper.DownloadToByteArray(id);
                        RepositoryLog.Log("MongoGridFSHelper.FindOneByIdAs", id, string.Format("{0}={1}", result.Id, fi.Length),
                            _databaseName, _collectionName);
                    }
                }
                return result;
            }
            catch (Exception ex)
            {
                RepositoryLog.Log("FindOneByIdAs", id, ex, _databaseName, _collectionName);
                throw;
            }
        }

        public bool Exists(Guid id)
        {
            try
            {
                var result = _repository.Exists(id);

                if (!result) return false;

                var fi = mongoGridFSHelper.FindOneById(id);

                return fi.Length > 0;
            }
            catch (Exception ex)
            {
                RepositoryLog.Log("FindOneByIdAs", id, ex, _databaseName, _collectionName);
                throw;
            }
        }

        public bool CreateBatch(IEnumerable<T> obj)
        {
            throw new NotImplementedException();
        }

        public bool CreateOrUpdateBatch(IEnumerable<T> inputs)
        {
            var ok = false;
            try
            {
                if (inputs == null || !inputs.Any())
                    throw new ArgumentOutOfRangeException("inputs");

                foreach (var entity in inputs.Where(x => x.FileContent != null))
                {
                    var gridFSResult = mongoGridFSHelper.UpdateFile(entity.FileName, entity.Id, entity.Id, entity.MimeType);
                    if (gridFSResult != null && gridFSResult.Item1 != null && gridFSResult.Item2 != Guid.Empty)
                    {
                        using (gridFSResult.Item1)
                            gridFSResult.Item1.Write(entity.FileContent, 0, entity.FileContent.Count());
                    }
                }

                ok = _repository.CreateOrUpdateBatch(inputs);
            }
            catch (Exception)
            {
                ok = false;
                throw;
            }
            finally // clean up if failures
            {
                if (!ok)
                {
                    foreach (var entity in inputs)
                    {
                        _repository.Delete(entity.Id);
                        mongoGridFSHelper.DeleteFile(entity.Id);
                    }
                }
            }

            return ok;
        }

        public IList<T> ReadAllWhere(Expression<Func<T, bool>> wherePredicate, int? skip = null, int? take = null, Ordering<T> ordering = null, bool useCollation = false, params string[] includeFields)
        {
            if (wherePredicate == null)
                throw new ArgumentNullException("wherePredicate");

            var entities = _repository.ReadAllWhere(wherePredicate, skip, take, ordering, useCollation, includeFields);
            ReadFileContent(entities);

            return entities;
        }

        private void ReadFileContent(IList<T> entities)
        {
            foreach (var e in entities)
            {
                var fi = mongoGridFSHelper.FindOneById(e.Id);
                if (fi != null)
                {
                    e.FileContent = mongoGridFSHelper.DownloadToByteArray(e.Id);
                    RepositoryLog.Log("MongoGridFSHelper.FindOneByIdAs", e.Id, string.Format("{0}={1}", e.Id, fi.Length), _databaseName, _collectionName);
                }
            }
        }

        public IList<T> ReadWhere(Expression<Func<T, bool>> wherePredicate, int? skip = null, int? take = null, Ordering<T> ordering = null, params Expression<Func<T, object>>[] includeFields)
        {
            return ReadAllWhere(wherePredicate, skip, take, ordering, false, includeFields.Select(x => x.GetPropertyName()).ToArray());
        }

        public bool Update(Guid id, T obj)
        {
            return CreateOrUpdate(obj);
        }

        public bool Delete(Guid id)
        {
            try
            {
                //mongoGridFSHelper.DeleteFile(id);
                return _repository.Delete(id);
            }
            catch (Exception ex)
            {
                RepositoryLog.Log("DeleteFile", id, ex, _databaseName, _collectionName);
                throw;
            }
        }

        public void Delete(IEnumerable<Guid> ids)
        {
            foreach (var id in ids)
                Delete(id);
        }

        public bool DeleteWhere(Expression<Func<T, bool>> wherePredicate)
        {
            return _repository.DeleteWhere(wherePredicate);
        }

        public IList<T> ReadAll()
        {
            throw new NotImplementedException();
        }

        public IList<T> ReadAllWhere(Expression<Func<T, bool>> wherePredicate, int? skip = null, int? take = null, Ordering<T> orderBy = null)
        {
            return ReadAllWhere(wherePredicate, skip, take, orderBy, false, new string[] { });
        }

        public IList<T> ReadAllWhereFieldEqualsValue<TK>(Expression<Func<T, TK>> selectPropertyExpression, TK value,
            int? skip = null, int? take = null, string[] includedFields = null)
        {
            var property = selectPropertyExpression.GetPropertyName();

            return ReadAllWhereFieldEqualsValue(property, value, skip, take, includedFields);
        }

        public IList<T> ReadAll(string[] includedFields, int? skip = null, int? take = null, Ordering<T> ordering = null, bool useCollation = false)
        {
            var entities = _repository.ReadAll(includedFields, skip, take, ordering, useCollation);
            ReadFileContent(entities);

            return entities;
        }

        public long ReadCount()
        {
            throw new NotImplementedException();
        }

        public bool DeleteAll()
        {
            try
            {
                mongoGridFSHelper.DeleteAllFiles();
                RepositoryLog.Log("RemoveAll", null, "GridFs.DeleteAll:ok", _databaseName, _collectionName);

                var result = _repository.DeleteAll();
                if (!result)
                    throw new RepositoryRemoveAllException(_databaseName, _collectionName);

                return true;
            }
            catch (RepositoryRemoveAllException ex)
            {
                RepositoryLog.Log("RemoveAll", "", ex, _databaseName, _collectionName);
                throw;
            }
        }

        public IDictionary<Guid, T> ReadbyIds(IEnumerable<Guid> ids, string[] includedFields = null)
        {
            throw new NotImplementedException();
        }

        public IList<T> ReadAllWhereFieldEqualsValue<TK>(string fieldName, TK value, int? skip = null, int? take = null,
            string[] includedFields = null)
        {
            var log = string.Empty;
            var entities = _repository.ReadAllWhereFieldEqualsValue(fieldName, value, skip, take, includedFields);
            if (entities != null)
            {
                foreach (var entity in entities)
                {
                    log = AddContentToFile(entity, log, out GridFSFileInfo fi);
                }
            }

            RepositoryLog.Log("ReadAllWhereFieldEqualValue", fieldName, log, _databaseName, _collectionName);
            return entities;
        }

        public bool UpdateBatch<TK>(Expression<Func<T, bool>> filter, params PropertyValuePair<T, TK>[] updateTo)
        {
            throw new NotImplementedException();
        }

        public IMongoCollection<T> Collection()
        {
            return _collection;
        }

        public string GetCollectionName()
        {
            return _collectionName;
        }

        public string GetDatabaseName()
        {
            return _databaseName;
        }

        private string AddContentToFile(T entity, string log, out GridFSFileInfo fi)
        {
            fi = mongoGridFSHelper.FindOneById(entity.Id);
            if (fi != null)
            {
                ((IFileContent)entity).FileContent = mongoGridFSHelper.DownloadToByteArray(entity.Id);
                log += string.Format("[{0}, {1}] ", entity.Id, fi.Length);
            }
            return log;
        }

        public long ReadCount(Expression<Func<T, bool>> wherePredicate)
        {
            throw new NotImplementedException();
        }

        public bool CollectionExists()
        {
            var filter = new BsonDocument("name", _collectionName);
            return _collection.Database.ListCollections(new ListCollectionsOptions { Filter = filter }).Any();
        }

        public bool CreateBatchParallel(IEnumerable<T> obj, int bulkInsertSize = 1000, int maxParalelism = -1)
        {
            throw new NotImplementedException();
        }
    }
}
